/*
*
* NHood
*
* This class represents a neighbourhood
*
*/

class NHood extends PlotObject {
  
  // ----------------------------------------
  // attributes
  // ---------------------------------------- 
  
  // "non-metric" attributes -- neighbourhood data not used as a metric
  String name, desc, nnum;
  ArrayList coords;
  ArrayList coordsScreen;
  
  // metric HashMap
  HashMap metricHash;
  
  // "simple metric" attributes -- data used for metrics that is directly assigned
  int[] femalePop = new int[19];
  int[] malePop = new int[19];
  int dwellingOwned, dwellingRented, dwellingNoResponse;
  int structureSingleDetached, structureDuplex, structureRowHouse, structureApartment5Plus, structureApartmentLess5;
  int structureManufacturedHome, structureRoomingHouse, structureHotel, structureOther, structureNoResponse;
  int statusOccupied, statusUnoccupied, statusVacantLot, statusUnderConstruction, statusNoLongerInUse, statusNoResponse;
  int aveIncome, medianIncome;
  float percentSingleDetached, percentDuplexRowHouse, percentApt5Plus, percentAptLess5;
  float percentOccupied, percentUnoccupied, percentVacantLot, percentUnderConstruction, percentNoLongerInUse;
  
  // "calculated metrics" attributes -- data used for metrics that is calculated
  float aveAge;
  int pop; 
  float percentOwn, percentRent;
  float peoplePerDwelling;
  float totalStructures;
  float area;
  float popDensity;
  float incomeDensity;
  
  // ------------------------------------------
  // constructor
  // ------------------------------------------
  NHood(String n, String d, String nn, ArrayList cs) {
    name = n;
    desc = d;
    nnum = nn;
    coords = cs;
    coordsScreen = new ArrayList();
    metricHash = new HashMap();
    area = 0;
    
    // CONDITION POLYGON DATA USING DOUGLAS-PEUCKER METHOD
    
    for(int i = 0; i < coords.size(); i++) {
      
      PVector[] poly;
      
      // get polygon 
      poly = (PVector[]) coords.get(i);     
      
      // apply douglasPeucker method;
      // second argument is tunable "epsilon" value; higher values remove
      // more points
      poly = douglasPeucker(poly, 2);
      
      // calculate area of polygon and add to neighbourhood's area attribute
      area += calculatePolygonArea(poly);
      
      // store
      coords.remove(i);
      coords.add(i, poly);
      
      // convert coordinates to screen space and store in coordsxy arraylist
      PVector[] polyScreen = new PVector[poly.length];
      for(int j = 0; j < poly.length; j++) {
        polyScreen[j] = convUTMToScreen(poly[j]);
      } // for
      
      coordsScreen.add(polyScreen);

    } // for
          
  }  // constructor method
  
  // ---------------------------------------------
  // importCensusData method
  // takes census data array and assigns to attributes
  // ---------------------------------------------
  void importCensusData(String[] cda) { 
    
    // assign data
    int sumAge = 0;
    pop = 0;
    
    // read population figures into arrays, and calculate total and average population
    for(int i = 1; i <= 19; i++) {
      femalePop[i - 1] = int(cda[i]);
      malePop[i - 1] = int(cda[i + 19]);
      pop += int(cda[i]) + int(cda[i + 19]);
      // don't add last item in array, as this is people who didn't specify an age
      if(i <= 18) {
        int tSum = int(cda[i]) + int(cda[i + 19]);
        sumAge += (2 + (i - 1) * 5) * tSum;
      } // if 
    } // for loop
    
    // check if population 0, or completely composed of "non specified" respondents
    // if so, assing "no data" flag value of -1
    if(pop == 0) {
      aveAge = -1;
    } else if((pop - int(cda[19]) - int(cda[38])) == 0 ) {
      aveAge = -1;    
    } else {
      // need to subtract numbers of people who didn't specify age when calculating average
      aveAge = float(sumAge) / float(pop - int(cda[19]) - int(cda[38]));
    } // if
    
    // directly assign remaining simple attributes
    dwellingOwned = int(cda[39]);
    dwellingRented = int(cda[40]);
    dwellingNoResponse = int(cda[41]);
    structureSingleDetached = int(cda[42]);
    structureDuplex = int(cda[43]);
    structureRowHouse = int(cda[44]);
    structureApartment5Plus = int(cda[45]);
    structureApartmentLess5 = int(cda[46]);
    structureManufacturedHome = int(cda[47]);
    structureRoomingHouse = int(cda[48]);
    structureHotel = int(cda[49]);
    structureOther = int(cda[50]);
    structureNoResponse = int(cda[51]);
    statusOccupied = int(cda[52]);
    statusUnoccupied = int(cda[53]);
    statusVacantLot = int(cda[54]);
    statusUnderConstruction = int(cda[55]);
    statusNoLongerInUse = int(cda[56]);
    statusNoResponse = int(cda[57]);
    
    // calculated attributes
    // a value of -1 is used as a flag to indicate that metric isn't applicable to neighbourhood in question
    if(((dwellingOwned == 0) && (dwellingRented == 0)) || ((dwellingOwned + dwellingRented) < 30)) {
      percentOwn = -1;
      percentRent = -1;
    } else {
      percentOwn = float(dwellingOwned) / float(dwellingOwned + dwellingRented) * 100;
      percentRent = float(dwellingRented) / float(dwellingOwned + dwellingRented) * 100;
    }
    if((dwellingOwned + dwellingRented + dwellingNoResponse) < 30) {
      peoplePerDwelling = -1;
    } else {
      peoplePerDwelling = float(pop) / float(dwellingOwned + dwellingRented + dwellingNoResponse);
    }
    
    totalStructures = structureSingleDetached + structureDuplex + structureRowHouse + structureApartment5Plus + structureApartmentLess5 +
      + structureManufacturedHome + structureRoomingHouse + structureHotel + structureOther + structureNoResponse;
    if(totalStructures == 0) {
      totalStructures = -1;
      percentSingleDetached = -1;
      percentDuplexRowHouse = -1; 
      percentApt5Plus = -1;
      percentAptLess5 = -1;
    } else {
      percentSingleDetached = structureSingleDetached / (totalStructures - structureNoResponse) * 100;
      percentDuplexRowHouse = (structureDuplex + structureRowHouse) / (totalStructures - structureNoResponse) * 100;
      percentApt5Plus = structureApartment5Plus / (totalStructures - structureNoResponse) * 100;
      percentAptLess5 = structureApartmentLess5 / (totalStructures - structureNoResponse) * 100;
    }
    
    int totalStatus = statusOccupied + statusUnoccupied + statusVacantLot + statusUnderConstruction + statusNoLongerInUse;
    if(totalStatus <= 0) {
      percentOccupied = -1;
      percentUnoccupied = -1;
      percentVacantLot = -1; 
      percentUnderConstruction = -1; 
      percentNoLongerInUse = -1;
    } else {
      percentOccupied = float(statusOccupied) / float(totalStatus) * 100;
      percentUnoccupied = float(statusUnoccupied) / float(totalStatus) * 100;
      percentVacantLot = float(statusVacantLot) / float(totalStatus) * 100;
      percentUnderConstruction = float(statusUnderConstruction) / float(totalStatus) * 100;
      percentNoLongerInUse = float(statusNoLongerInUse) / float(totalStatus) * 100;
    } // if

    // population density
    if(pop == 0) {
      popDensity = -1;
    } else {
      popDensity = pop/area;
    }
    
  } // importCensusData
  
  // -----------------------------------------
  // importOtherData 
  // takes "other" data array and assigns to attributes
  // -----------------------------------------
  void importOtherData(String[] oda) { 
    
    aveIncome = int(oda[1]);
    medianIncome = int(oda[2]);
    
    // income density ($1000/km2)
    if(pop <= 0 || aveIncome <= 0) {
      incomeDensity = -1;
    } else {
      incomeDensity = aveIncome * pop / area / 1000;
    }  
    
  } // importOtherData
    
  // importNullCensusData method -- for "neighbourhoods" in KML file without census data; sets all
  // attributes to -1, which is used a flag for "no data"
  void importNullCensusData() {
    
    // simple attributes
    dwellingOwned = -1;
    dwellingRented = -1;
    dwellingNoResponse = -1;
    structureSingleDetached = -1;
    structureDuplex = -1;
    structureRowHouse = -1;
    structureApartment5Plus = -1;
    structureApartmentLess5 = -1;
    structureManufacturedHome = -1;
    structureRoomingHouse = -1;
    structureHotel = -1;
    structureOther = -1;
    structureNoResponse = -1;
    statusOccupied = -1;
    statusUnoccupied = -1;
    statusVacantLot = -1;
    statusUnderConstruction = -1;
    statusNoLongerInUse = -1;
    statusNoResponse = -1;
    
    // calculated attributes
    pop = -1;
    aveAge = -1;
    percentOwn = -1;
    percentRent = -1;
    peoplePerDwelling = -1;
    
    totalStructures = -1;
    percentSingleDetached = -1;
    percentDuplexRowHouse = -1; 
    percentApt5Plus = -1;
    percentAptLess5 = -1;
    
    percentOccupied = -1;
    percentUnoccupied = -1;
    percentVacantLot = -1; 
    percentUnderConstruction = -1; 
    percentNoLongerInUse = -1;
    
    popDensity = -1;
    
  } // importNullCensusData
  
  // -----------------------------------------------------
  // importNullOtherData
  // see importNullCensusData for the purpose of this method;
  // this one does the same thing, but with data from a different source
  // ------------------------------------------------------
  void importNullOtherData() {
    
    aveIncome = -1;
    medianIncome = -1;
    
    incomeDensity = -1;
    
  } // importNullOtherData
  
  // ------------------------------------------------------
  // makeHash
  // builds the MetricHash from data calculated in the above methods
  // ------------------------------------------------------
  void makeHash() {
    
    metricHash.put("female0-4", float(femalePop[0]));
    metricHash.put("female5-9", float(femalePop[1]));
    metricHash.put("female10-14", float(femalePop[2]));
    metricHash.put("female15-19", float(femalePop[3]));
    metricHash.put("female20-24", float(femalePop[4]));
    metricHash.put("female25-29", float(femalePop[5]));
    metricHash.put("female30-34", float(femalePop[6]));
    metricHash.put("female35-39", float(femalePop[7]));
    metricHash.put("female40-44", float(femalePop[8]));
    metricHash.put("female45-49", float(femalePop[9]));
    metricHash.put("female50-54", float(femalePop[10]));
    metricHash.put("female55-59", float(femalePop[11]));
    metricHash.put("female60-64", float(femalePop[12]));
    metricHash.put("female65-69", float(femalePop[13]));
    metricHash.put("female70-74", float(femalePop[14]));
    metricHash.put("female75-79", float(femalePop[15]));
    metricHash.put("female80-84", float(femalePop[16]));
    metricHash.put("female85", float(femalePop[17]));
    metricHash.put("femaleNoResponse", float(femalePop[18]));
    metricHash.put("male0-4", float(malePop[0]));
    metricHash.put("male5-9", float(malePop[1]));
    metricHash.put("male10-14", float(malePop[2]));
    metricHash.put("male15-19", float(malePop[3]));
    metricHash.put("male20-24", float(malePop[4]));
    metricHash.put("male25-29", float(malePop[5]));
    metricHash.put("male30-34", float(malePop[6]));
    metricHash.put("male35-39", float(malePop[7]));
    metricHash.put("male40-44", float(malePop[8]));
    metricHash.put("male45-49", float(malePop[9]));
    metricHash.put("male50-54", float(malePop[10]));
    metricHash.put("male55-59", float(malePop[11]));
    metricHash.put("male60-64", float(malePop[12]));
    metricHash.put("male65-69", float(malePop[13]));
    metricHash.put("male70-74", float(malePop[14]));
    metricHash.put("male75-79", float(malePop[15]));
    metricHash.put("male80-84", float(malePop[16]));
    metricHash.put("male85", float(malePop[17]));
    metricHash.put("maleNoResponse", float(malePop[18]));
    metricHash.put("dwellingOwned", float(dwellingOwned));
    metricHash.put("dwellingRented", float(dwellingRented));
    metricHash.put("dwellingNoResponse", float(dwellingNoResponse));
    metricHash.put("structureSingleDetached", float(structureSingleDetached));
    metricHash.put("structureDuplex", float(structureDuplex));
    metricHash.put("structureRowHouse", float(structureRowHouse));
    metricHash.put("structureApartment5Plus", float(structureApartment5Plus));
    metricHash.put("structureApartmentLess5", float(structureApartmentLess5));
    metricHash.put("structureManufacturedHome", float(structureManufacturedHome));
    metricHash.put("structureRoomingHouse", float(structureRoomingHouse));
    metricHash.put("structureHotel", float(structureHotel));
    metricHash.put("structureOther", float(structureOther));
    metricHash.put("structureNoResponse", float(structureNoResponse));
    metricHash.put("statusOccupied", float(statusOccupied));
    metricHash.put("statusUnoccupied", float(statusUnoccupied));
    metricHash.put("statusVacantLot", float(statusVacantLot));
    metricHash.put("statusUnderConstruction", float(statusUnderConstruction));
    metricHash.put("statusNoLongerInUse", float(statusNoLongerInUse));
    metricHash.put("statusNoResponse", float(statusNoResponse));
    metricHash.put("aveIncome", float(aveIncome));
    metricHash.put("medianIncome", float(medianIncome));
    metricHash.put("percentSingleDetached", percentSingleDetached);
    metricHash.put("percentDuplexRowHouse", percentDuplexRowHouse);
    metricHash.put("percentApt5Plus", percentApt5Plus);
    metricHash.put("percentAptLess5", percentAptLess5);
    metricHash.put("percentOccupied", percentOccupied);
    metricHash.put("percentUnoccupied", percentUnoccupied);
    metricHash.put("percentVacantLot", percentVacantLot);
    metricHash.put("percentUnderConstruction", percentUnderConstruction);
    metricHash.put("percentNoLongerInUse", percentNoLongerInUse);
    metricHash.put("aveAge", aveAge);
    metricHash.put("pop", float(pop));
    metricHash.put("percentOwn", percentOwn);
    metricHash.put("percentRent", percentRent);
    metricHash.put("peoplePerDwelling", peoplePerDwelling);
    metricHash.put("totalStructures", totalStructures);
    metricHash.put("popDensity", popDensity);
    metricHash.put("incomeDensity", incomeDensity);    
    
  } // makeHash
  
  // ------------------------------------------------------
  // draw
  // called during each iteration of draw loop
  // ------------------------------------------------------
  void draw(DModeManager dModeMan) {
    
    // declare variables
    PVector[] polyScreen;
    
    // iterate through polygons
    for(int i = 0; i < coords.size(); i++) {
      
      // get polygon 
      polyScreen = (PVector[]) coordsScreen.get(i);
      
      // draw polygon
      setColour(dModeMan);
      beginShape();
      
      for(int j = 0; j < polyScreen.length; j++) {

        vertex(polyScreen[j].x, polyScreen[j].y);
        
        // DEBUG -- PLOT DATA POINTS
//        stroke(0, 100); ////////////////////////
//        point(polyScreen[j].x, polyScreen[j].y); ///////////////////
//        stroke(50, 50, 50, 75); ////////////////
        
      } // for loop j
      
      endShape(CLOSE);
      
    } // for loop i
      
  } // draw method
  
  // -------------------------------------------------
  // setColour
  // retrieves fill and stroke colours based on drawing mode
  // -------------------------------------------------
  void setColour(DModeManager dModeMan) {
    
    DMode dMode = dModeMan.getDMode();
    
    // check if drawing mode has metric; if so, retrieve colour and set;
    // if not, set colour to default value
    if(dMode.hasMetric) {
      
      // retrieve metric value
      float metricVal = (Float) metricHash.get(dMode.hashName);
      
      // check if metric has no data flag; if so, set colours to "no data" value;
      // if not, retrieve drawing mode colour and set
      if(metricVal == -1) {
        fill(noDataFil);
        stroke(strok);
      } else {
        color col = dMode.getColour(metricVal);
        fill(col);
        stroke(strok);
      } // if
    } else {
      fill(dFil);
      stroke(strok);
    } // if 

  } // setColour method
  
  // -----------------------------------------
  // douglasPeucker
  // this method is used to remove unnecessary points
  // from the plot, using the Douglas-Peucker algorithm
  // -----------------------------------------
  PVector[] douglasPeucker (PVector[] points, float epsilon) {
    
    // declare variables
    float dmax, d;
    int index;
    PVector[] points1, points2, out;
    
    // calculate point with maximum orthogonal distance from line
    // between first and last point in "points"
    dmax = 0;
    index = 0;
    
    for(int i = 1; i < (points.length - 1); i++) {
      
      // check if first and last point are coincident (which will be the case for closed loops)
      // if so, use absolute distance from the coincident points in place of orhotgonal distance
      if(((points[0].x - points[points.length - 1].x) == 0) && ((points[0].y - points[points.length - 1].y) == 0)) {
        d = sqrt(sq(points[i].x - points[0].x) + sq(points[i].y - points[0].y));
      } else {
        // calculate orthogonal distance; store largest distance and index of point
        d = orthogonalDistance(points[i], points[0], points[points.length - 1]);
      }
      if(d > dmax) {
        dmax = d;
        index = i;
      } // if
    } // for
    
    // if dmax is greather than epsilon (tunable parameter), divide points into arrays
    // at location of dmax and recursively call method on both arrays
    if(dmax >= epsilon) {
      points1 = douglasPeucker(((PVector[]) subset(points, 0, (index + 1))), epsilon);
      points2 = douglasPeucker(((PVector[]) subset(points, index, (points.length - index))), epsilon);
      
      // store points calculate above in output variable
      out = (PVector[]) concat(shorten(points1), points2);
    } else {
      // if no points fall outside dmax, return only first and last points in "points"
      out = new PVector[2];
      out[0] = points[0];
      out[1] = points[points.length - 1];
    } // if
    
    // return output variable
    return out;
    
  } // douglasPeucker algorithm
  
  // ------------------------------------
  // orthogonalDistance
  // calculates orthogonal distance between a line
  // and a point. Used for douglasPeucker method above
  // ------------------------------------
  float orthogonalDistance(PVector poin, PVector firstPoint, PVector lastPoint) {
    
    // declare variables
    float out;
    //float[] v1, v2;
    float dotProd, l1, l2, sinTheta;
    
    // convert lat/long to pixel coordinates
    
    // calculate dot product of vectors between firstPoint and poin (v1) and
    // firstPoint and lastPoint (v2)
    PVector v2 = PVector.sub(lastPoint, firstPoint);
    PVector v1 = PVector.sub(poin, firstPoint);
    dotProd = v1.dot(v2);

    // calculate lengths between firstPoint and poin (l1) and firstPoint and
    // lastPoint (l2)
    l1 = v1.mag();
    l2 = v2.mag();
    
    // calculate sin of angle between v1 and v2. if dotProd = 0, set sinTheta to 0
    if(dotProd == 0) {
      sinTheta = 0;
    } else {
      sinTheta = sqrt(1 - sq(dotProd / (l1 * l2)));
    } // if
    // if sinTheta very small, set to 0
    // this is a bit of a hack
    if(sinTheta < 0.001) {
      sinTheta = 0;
    } // if
    
    // calculate distance and return;
    out = l1 * sinTheta;
    return out;    
    
  } // orthogonalDistance
  
  // ---------------------------------------
  // checkMouseOver
  // checks if mouse is positioned over neighbourhood
  // if so, displays neighbourhood name, description, and
  // current metric value
  // ---------------------------------------
  void checkMouseOver(PVector poin, NHoodInfoUI nhui, DModeManager dModeMan) {
    
    // iterate through neighbourhood's polygons and check each
    for(int i = 0; i < coords.size(); i++) {
      
      PVector[] polyScreen;
      String sCurVal = "";
      float curVal = -2;
      DMode dMode = dModeMan.getDMode();
      
      // get polygon and remove last (duplicate) coordinate
      polyScreen = (PVector[]) coordsScreen.get(i);
      polyScreen = (PVector[]) shorten(polyScreen);      
      
      // test polygon; if mouse is inside, update neighbourhood information
      if(pointInPolygon(polyScreen, poin)) {
        
        // check if drawing mode has metric; if not, set curVal to "blank flag" value;
        // if so, retrieve current metric value and store in curVal
        if(dMode.hasMetric) {
          curVal = (Float) metricHash.get(dMode.hashName);
        } else {
          curVal = -2;
        } // if
        
        // check for "no data" flag, or 
        // flag indicating field should be left blank
        if(curVal == -1) {
          sCurVal = "no data";
        } else if(curVal == -2) {
          sCurVal = "";
        } else {
          sCurVal = nf(curVal, 3, 1);
        } // if
        
        // update neighbourhood information
        nhui.drawDynamic(name, sCurVal);
        
      } // if
      
    } // for
    
  } //checkMouseOver
  
  // ----------------------------------------
  // pointInPolygon
  // checks if inputted point is within polygon;
  // returns true if so, false if not
  // ----------------------------------------
  boolean pointInPolygon(PVector[] poly, PVector poin) {
    
    int i;
    int j = poly.length - 1;
    boolean oddNodes = false;
    
    for(i = 0; i < poly.length; i++) {
      
      if(((poly[i].y < poin.y) && (poly[j].y >= poin.y)) ||
          ((poly[j].y < poin.y) && (poly[i].y >= poin.y))) {
            if((poly[i].x + (poin.y - poly[i].y) / (poly[j].y - poly[i].y) * (poly[j].x - poly[i].x)) < poin.x) {
            if(oddNodes) {
              oddNodes = false;
            } else {
              oddNodes = true;
            } // if
          } // if
        } // if
        
        j = i;
    } // for
    
    return oddNodes;    
    
  } // pointInPolygon
  
  // ----------------------------------------
  // calculatePolygonArea
  // note: assumes that input polygon coordinates are in metres
  // also note: assumes input polygons are traced counter-clockwise
  // this is based on an algorithm written in C by Darel Rex Finlay
  // see http://www.alienryderflex.com/polygon_area/
  // ----------------------------------------
  float calculatePolygonArea(PVector[] poly) {
  
    float out = 0;
    int j = 0;
    
    // iterate through polygon segments and calculate the area
    // between each segment and y-axis
    for(int i = 0; i < poly.length; i++) {
      
      j += 1;
      if(j==poly.length) j=0;
      
      out += (poly[i].x + poly[j].x) * (poly[j].y - poly[i].y);
    } // for
        
    // convert area from square metres to square kilometres and return
    return out/1000000;
    
  } // calculatePolygonArea
  
} // class

